前面幾天,我們已經使用 Express 建立 Route,完成 Note API 的 CRUD,也把不同的 Route 拆分到 Router 中。現在 API 已經可以正常接收 Request、處理資料,再回傳 Response,不過在實際開發中,一個 Request 通常不會直接進入 Route Handler,而是會先經過一些額外的處理流程。
例如:
404 Not Found
這些工作雖然不是 API 本身的主要功能,卻可能需要在 Request 處理過程中執行。
Express 提供的 Middleware,就是用來處理這類共同或額外的流程。
Middleware 可以理解成 Express Request 處理流程中的中間處理函式。Request 進入 Express 後,可以先經過一個或多個 Middleware,再繼續往後執行。
Middleware 通常會接收 req、res、next 三個參數:
req 是 Request,可以用來取得前端送來的請求資料res 是 Response,可以用來回傳結果next 則是用來把目前的處理交給下一個 Middleware 或 Route。function middleware(req, res, next) {
// ...
}
因此,Middleware 可以先處理自己的工作,再決定這個 Request 要不要繼續往下走。正常情況下,處理完成後會呼叫 next():
Request
↓
Middleware
↓
next()
↓
繼續往後處理
但如果 Middleware 已經可以直接結束這次 Request,就不需要呼叫 next(),例如驗證失敗時,可以直接回傳 401:
function checkAuth(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({
message: "請先登入"
});
}
next();
}
這裡沒有 Token 時,Middleware 會直接回傳 Response,Request 就在這裡結束;有 Token 時,才會呼叫 next() 繼續往後處理。
了解 Middleware 的基本概念後,可以用 Logger 來看看實際的 Request 流程。假設我們希望每次 Request 進來時,都記錄 Method 和 URL,可以建立一個 Logger Middleware:
const express = require("express");
const app = express();
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}
app.use(logger);
app.get("/notes", (req, res) => {
res.json([
{ id: 1, title: "第一篇筆記" },
{ id: 2, title: "第二篇筆記" }
]);
});
app.listen(3000);
當前端送出:
GET /notes
Request 會先進入 logger,這時 req.method 取得的是 GET,req.url 取得的是 /notes,所以 Console 會印出:
GET /notes
Logger 完成工作後呼叫 next(),Express 才會繼續往後尋找可以處理這個 Request 的 Route。找到 /notes 後,就會執行 Route Handler,最後回傳 Response。
Request
↓
logger
↓
next()
↓
/notes Route
↓
Response
這就是 Middleware 的基本用途。它可以在 Request 往後處理的過程中加入額外工作,而不需要把這些邏輯全部寫進 Route Handler。
Middleware 會依照註冊的順序執行,所以放置的位置非常重要。例如:
app.use((req, res, next) => {
console.log("Middleware 1");
next();
});
app.use((req, res, next) => {
console.log("Middleware 2");
next();
});
app.get("/notes", (req, res) => {
console.log("Route");
res.json({ message: "Hello" });
});
當 Request 進入 /notes 時,會依序執行 Middleware 1、Middleware 2 和 Route Handler,因此 Console 會看到:
Middleware 1
Middleware 2
Route
也就是說,如果某個 Middleware 必須先處理,就要放在需要它的 Route 前面。這個概念在 404 Middleware 特別重要,因為它需要等前面的 Route 都沒有處理 Request 後,才能接手。
Middleware 要放在哪裡,主要取決於它需要被多少地方使用。如果整個 Application 都需要,就可以掛在 app 上;如果只有某一組 API 需要,就可以掛在 Router 上;如果只有特定 Route 需要,也可以直接放進 Route 的處理流程。
如果 Middleware 希望整個 Application 都可以使用,可以透過 app.use() 註冊:
app.use(logger);
例如:
const express = require("express");
const app = express();
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}
app.use(logger);
app.get("/notes", (req, res) => {
res.json({ message: "Notes" });
});
app.get("/users", (req, res) => {
res.json({ message: "Users" });
});
app.listen(3000);
這樣 /notes、/users 等符合條件的 Request 都會先經過 logger。這種方式適合處理整個 Application 都可能需要的功能,例如 Logger 或 CORS。
如果 Middleware 只需要套用在某一組 API,可以把它放在 Router 上。假設 Notes 相關 API 都需要先做登入檢查,可以使用 router.use():
const express = require("express");
const router = express.Router();
function checkAuth(req, res, next) {
console.log("檢查登入狀態");
next();
}
router.use(checkAuth);
router.get("/", (req, res) => {
res.json({ message: "取得 Notes" });
});
router.post("/", (req, res) => {
res.json({ message: "建立 Note" });
});
module.exports = router;
再由 app.js 掛載 Router:
const express = require("express");
const notesRouter = require("./routes/notes");
const app = express();
app.use("/notes", notesRouter);
app.listen(3000);
這樣進入 /notes Router 的 Request 都會先經過 checkAuth,其他 Router 則不會受到影響。
因此,app.use() 和 router.use() 可以先理解成兩種不同的使用範圍:
app.use()
→ 整個 Application 共用
router.use()
→ 某一組 API 共用
有時候只有某一條 API 需要額外處理,就不需要把 Middleware 套用到整個 Router,也可以直接把多個 callback 放進 Route。
例如:
router.get("/", checkAuth, (req, res) => {
res.json({
message: "取得 Notes"
});
});
這裡 checkAuth 會先執行,如果檢查成功並呼叫 next(),才會繼續執行後面的 Route Handler。也可以放入多個 callback:
router.post("/", checkAuth, validateNote, (req, res) => {
res.json({
message: "建立 Note"
});
});
這些函式會按照順序執行,所以可以把需要先處理的工作放在 Route Handler 前面。這種寫法不需要另外理解成一種 Middleware 類型,只要知道 Route 可以接收多個 callback,讓不同的處理工作依序執行即可。
Middleware 也可以用來處理沒有符合 Route 的 Request。假設現在只有 /notes 這個 Route:
app.get("/notes", (req, res) => {
res.json({
message: "取得 Notes"
});
});
如果前端請求 /users,因為沒有任何 Route 可以處理這個 URL,Request 就會繼續往後走。這時可以在所有 Route 後面加入一個 Middleware:
app.use((req, res) => {
res.status(404).json({
message: "找不到此 API"
});
});
這個 Middleware 會接住前面沒有被處理的 Request,因此 /users 最後就會得到 404 Not Found。它最重要的不是寫法,而是放置的位置,因為必須等前面的 Route 都沒有符合之後,才輪得到它處理。
除了找不到 Route,程式執行過程中也可能發生錯誤。這時候可以使用 Error-handling Middleware 集中處理:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
message: "伺服器發生錯誤"
});
});
這個 Middleware 和一般 Middleware 不同,它會接收四個參數:err、req、res、next。當前面的程式發生錯誤時,可以透過 next(error) 把錯誤交給它處理:
app.get("/notes", (req, res, next) => {
try {
// 執行可能發生錯誤的程式
} catch (error) {
next(error);
}
});
這樣錯誤就可以集中處理,不需要每個 Route 都自己處理相同的錯誤回應。
前面已經知道 Middleware 可以放在 Application、Router 或特定 Route 的處理流程中。實際開發時,通常會依照功能拆成不同的 Middleware,讓每個 Middleware 負責自己的工作。
例如專案可以規劃成:
project/
├── app.js
├── routes/
│ ├── notes.js
│ └── users.js
├── middlewares/
│ ├── logger.js
│ ├── auth.js
│ └── validateNote.js
└── package.json
其中 logger.js 負責記錄 Request,auth.js 負責驗證使用者,validateNote.js 負責檢查資料格式。
app.js 則負責整個 Application 的設定,例如註冊共用的 Middleware、掛載 Router,以及處理找不到 Route 和程式錯誤的情況:
const express = require("express");
const notesRouter = require("./routes/notes");
const logger = require("./middlewares/logger");
const app = express();
app.use(express.json());
app.use(logger);
app.use("/notes", notesRouter);
// 404 Middleware
app.use((req, res) => {
res.status(404).json({
message: "找不到此 API"
});
});
// Error-handling Middleware
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
message: "伺服器發生錯誤"
});
});
app.listen(3000);
整體架構可以簡單理解成:
Application
│
├── 共用 Middleware
│
├── Router
│ ├── Router Middleware
│ └── Route Handler
│
├── 404 Middleware
│
└── Error-handling Middleware
Middleware 會按照註冊順序往下執行,因此設計時除了考慮「誰需要使用」,也要考慮「什麼時候需要執行」。例如共用的 Middleware 通常會放在 Route 前面,404 Middleware 則要放在所有 Route 後面,這樣才能接住前面沒有處理的 Request。
所以設計 Middleware 時,可以先記住一個簡單原則:
需要共用的功能放外層,需要局部使用的功能放內層,需要最後處理的情況就放在後面。
這樣當 API 越來越多時,就可以把不同的處理工作分開管理,讓 Middleware 和 Route Handler 各自負責自己的功能。
Middleware 可以把 Request 處理過程中的額外工作獨立出來,像是記錄 Request、驗證或資料檢查。實際使用時,先看這個功能需要被多少地方共用,再決定要放在 Application、Router,還是特定 Route 中。
當 API 越來越多時,這樣的設計可以減少重複程式碼,也讓每個 Route 的責任更加清楚。